package de.fhkl.whatsstroke;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.util.Date;
import java.util.UUID;

import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.os.StrictMode;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import de.fhkl.strokeprotocol.io.StrokeInputReader;
import de.fhkl.strokeprotocol.io.StrokeOutputWriter;
import de.fhkl.strokeprotocol.listeners.ICloseListener;
import de.fhkl.strokeprotocol.listeners.IMessageListener;
import de.fhkl.strokeprotocol.messages.AcceptRatingMessage;
import de.fhkl.strokeprotocol.messages.AcceptVideoMessage;
import de.fhkl.strokeprotocol.messages.IMessage;
import de.fhkl.strokeprotocol.messages.LoginAnswerMessage;
import de.fhkl.strokeprotocol.messages.LoginMessage;
import de.fhkl.strokeprotocol.messages.LogoutMessage;
import de.fhkl.strokeprotocol.messages.PingMessage;
import de.fhkl.strokeprotocol.messages.RatingMessage;
import de.fhkl.strokeprotocol.messages.RecordingStartsMessage;
import de.fhkl.strokeprotocol.messages.RecordingStopsMessage;
import de.fhkl.strokeprotocol.messages.TransmissionCompletedMessage;
import de.fhkl.strokeprotocol.messages.VideoMessage;
import de.fhkl.strokeprotocol.messages.VideoStartsMessage;

/**
 * Underlying service of WhatsStroke for network communication with the
 * StrokeServer.
 */
public class StrokeService extends Service implements ICloseListener
{
	/** Possible system states */
	private static enum SystemStatus
	{
		CONTACT_AVAILABLE, SERVER_AVAILABLE, OFFLINE
	}

	/** Unique identifier for status notifications */
	private static final int STATUS_NOTIFICATION = 1;

	/** Time interval between pings */
	private static final int PING_INTERVAL = 30000;

	/** Time to wait before the service will try to reconnect */
	private static final int TIMEOUT_INTERVAL = 10000;

	/** Maximum size of one part of video data */
	private static final int PACKET_SIZE = 128000;

	/** Constants for shared preference identifiers */
	private static final String SERVER_ADDRESS = "serverAddress";
	private static final String SERVER_PORT = "serverPort";

	private static final String STATE = "state";
	private static final String DESCRIPTION = "description";
	private static final String RATING = "rating";
	private static final String FILE_SIZE = "fileSize";
	private static final String PROGRESS = "progress";
	private static final String VIDEO_ID = "videoId";
	private static final String VIDEO_PATH = "videoPath";

	/**
	 * Vibrator pattern starting with a pause followed by a vibrate in
	 * milliseconds.
	 */
	private static final long[] RRRRING = { 80, 100, 200, 100, 200, 100, 200, 100, 50 };

	/**
	 * Given user email, this is necessary to reconnect after a connection loss
	 */
	private String mEmail;

	/**
	 * Given user password, this is necessary to reconnect after a connection
	 * loss
	 */
	private String mPassword;

	/** Server address */
	private InetAddress mInetAddress;

	/** Server port, default is 50001 */
	private int mPort;

	/** Socket for the connection to the server */
	private Socket mSocket;

	/** Output stream to send messages to the server */
	private StrokeOutputWriter mOut;

	/** Input stream to listen for messages from the server */
	private StrokeInputReader mInputListener;

	/** Is there an open connection to the server? */
	private boolean mIsConnected;

	/** Is the user already successfully logged in to the server? */
	private boolean mIsLoggedIn;

	/** We use shared preferences to persist some data */
	private SharedPreferences mPreferences;

	/** Ping timer */
	private PingTimer mPingTimer;

	/** Timeout timer */
	private TimeoutTimer mTimeoutTimer;

	/** Notification manager allows us to set notifications in the icon tray */
	private NotificationManager mNotManager;

	/** Current status notification */
	private Notification mNotification;

	/** Previous system status */
	private SystemStatus mPreviousStatus;

	/**
	 * If mCancelVideoTransmission is true a running video transmission will be
	 * stopped
	 */
	private boolean mCancelVideoTransmission;

	/**
	 * Thread which performs the video transmission. To stop this thread and
	 * cancel the video transmission set mCancelVideoTransmission to true.
	 * 
	 * @see cancelVideoTransmission()
	 */
	private Thread mVideoTransmissionThread;

	@TargetApi(Build.VERSION_CODES.GINGERBREAD)
	@Override
	public void onCreate()
	{
		Log.i("Service", "onCreate " + this);

		super.onCreate();

		// Suppresses network on main thread exceptions implemented in version
		// 3.0. Testing the app mostly on 2.2 to 2.3 devices did not show any
		// strange errors occurring from the blocking network connection. You
		// should reconsider this when continuing development. Should be fine
		// for evaluation prototype.
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD)
		{
			StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
			StrictMode.setThreadPolicy(policy);
		}

		mPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());

		// Delete old or broken video files
		deleteOldVideos();

		mPingTimer = new PingTimer();
		mTimeoutTimer = new TimeoutTimer();

		registerReceiver(mPingTimer, new IntentFilter(Broadcast.PING_TIMER));
		registerReceiver(mTimeoutTimer, new IntentFilter(Broadcast.TIMEOUT_TIMER));
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId)
	{
		Log.i("Service", "onStartCommand " + this);

		super.onStartCommand(intent, flags, startId);

		return START_STICKY;
	}

	@Override
	public void onDestroy()
	{
		this.unregisterReceiver(mPingTimer);
		this.unregisterReceiver(mTimeoutTimer);

		super.onDestroy();
	}

	@Override
	public IBinder onBind(Intent intent)
	{
		mNotManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

		Log.i("Service", "onBind");

		return new StrokeBinder(this);
	}

	@Override
	public void onClose(Object obj)
	{
		setNotification(SystemStatus.OFFLINE);

		Log.i("Service", "onClose");

		mTimeoutTimer.startBackOff();
	}

	/**
	 * Sets the correspondent notification for the given status in the icon
	 * tray.
	 * 
	 * @param status
	 */
	@SuppressWarnings("deprecation")
	private void setNotification(SystemStatus status)
	{
		// if the status has changed since the last call of this method
		if (mPreviousStatus != status)
		{
			// set previous status
			mPreviousStatus = status;

			// remove old notification before setting a new one
			mNotManager.cancel(STATUS_NOTIFICATION);

			// create new notification by given status
			mNotification = new Notification();
			mNotification.setLatestEventInfo(this, "WhatsStroke", "Aufnahme bei Verdacht auf Schlaganfall",
					PendingIntent.getActivity(this, 0, new Intent(this, Login.class), 0));

			switch (status)
			{
				case CONTACT_AVAILABLE:
					mNotification.icon = R.drawable.status_contact_available;
				break;

				case SERVER_AVAILABLE:
					mNotification.icon = R.drawable.status_server_available;
				break;

				case OFFLINE:
					mNotification.icon = R.drawable.status_offline;
			}
		}

		// set the notification
		mNotManager.notify(STATUS_NOTIFICATION, mNotification);
	}

	/**
	 * Try to login on the specified server with the given email and password.
	 * 
	 * @param email
	 * @param password
	 * 
	 * @return boolean Indicates that the connection to the server was
	 *         successfully established, NOT a successful login.
	 */
	public boolean login(String email, String password)
	{
		try
		{
			if (!mIsConnected)
			{
				if (!connect())
				{
					return false;
				}
			}

			mEmail = email;
			mPassword = password;

			Log.i("Service", "Login");

			mOut.send(new LoginMessage(email, password, 'p'));
		}
		catch (Exception e)
		{
			Log.e("Service ", "Exception " + e.getMessage());

			return false;
		}

		return true;
	}

	/**
	 * Try to log out from the server and stop this service.
	 */
	public void logout()
	{
		try
		{
			mOut.send(new LogoutMessage());

			disconnect();
		}
		catch (Exception e)
		{
			Log.e("Service", "Exception " + e.getMessage());
		}

		mIsLoggedIn = false;
		mEmail = null;
		mPassword = null;

		mNotManager.cancel(STATUS_NOTIFICATION);
	}

	/**
	 * Tells the server that a video recording with given id has started.
	 * 
	 * @param videoId
	 * @return
	 */
	public boolean videoRecordingStarts(UUID videoId)
	{
		Log.i("Service", "Recording starts " + videoId);

		return sendMessage(new RecordingStartsMessage(videoId));
	}

	/**
	 * Tells the server that a video recording with given id has stopped.
	 * 
	 * @param videoId
	 * @return
	 */
	public boolean videoRecordingStops(UUID videoId)
	{
		Log.i("Service", "Recording stops " + videoId);

		Editor editor = mPreferences.edit();
		editor.putString(VIDEO_ID, videoId.toString());
		editor.commit();

		return sendMessage(new RecordingStopsMessage(videoId));
	}

	/**
	 * Sends the specified video to the server.
	 * 
	 * @param videoId
	 * @param videoData
	 */
	public void sendVideo(final UUID videoId, final File file)
	{
		Log.i("Service", "Transmitting video " + videoId);

		mCancelVideoTransmission = false;

		Editor editor = mPreferences.edit();
		editor.putLong(FILE_SIZE, file.length());
		editor.commit();

		mVideoTransmissionThread = new Thread()
		{
			@Override
			public void run()
			{
				mTimeoutTimer.cancel();
				mPingTimer.cancel();

				FileInputStream fis = null;

				try
				{
					fis = new FileInputStream(file);

					byte[] videoData = new byte[PACKET_SIZE];

					int read;
					while ((read = fis.read(videoData)) > 0 && !mCancelVideoTransmission)
					{
						if (read < PACKET_SIZE)
						{
							byte[] restData = new byte[read];
							for (int i = 0; i < read; i++)
							{
								restData[i] = videoData[i];
							}

							videoData = restData;
						}

						// has the transmission been aborted or has it been
						// successful?
						if (sendMessage(new VideoMessage(videoId, videoData), 0))
						{
							Editor editor = mPreferences.edit();
							editor.putLong(PROGRESS, mPreferences.getLong(PROGRESS, 0) + read);
							editor.commit();

							Intent intent = new Intent("de.fhkl.whatsstroke.VIDEO_TRANSMISSION_PROGRESS");
							intent.putExtra("de.fhkl.whatsstroke.VideoTransmissionProgress", read);
							sendBroadcast(intent);
						}

						// IMPORTANT!!!
						// reset video data to prevent serializer from using a
						// back-reference pointing to a previously serialized
						// instance
						videoData = new byte[PACKET_SIZE];
					}

					if (!mCancelVideoTransmission && sendMessage(new TransmissionCompletedMessage(videoId), 0))
					{
						// tell the server that the video transmission is
						// complete
						Editor editor = mPreferences.edit();
						editor.putLong(PROGRESS, file.length());
						editor.putInt(STATE, TransmissionState.WAIT_FOR_RATING.ordinal());
						editor.commit();

						Log.i("Service", "Transmission complete " + videoId);
						Intent intent = new Intent("de.fhkl.whatsstroke.UPDATE_VIEW");
						sendBroadcast(intent);
					}
				}
				catch (IOException e)
				{
					Log.e("Service", "IOException: " + e.getMessage());
				}
				finally
				{
					if (fis != null)
					{
						try
						{
							// close file input stream
							fis.close();
							fis = null;
						}
						catch (IOException e)
						{
							Log.e("Service", "Exception: " + e.getMessage());
						}
					}

					// restart ping
					mPingTimer.start(PING_INTERVAL);
				}
			}
		};

		mVideoTransmissionThread.start();
	}

	/**
	 * Cancels a running video transmission.
	 */
	public void cancelVideoTransmission()
	{
		try
		{
			mCancelVideoTransmission = true;

			// stop video transmission thread if its not already stopped
			if (mVideoTransmissionThread != null)
			{
				mVideoTransmissionThread.join();
				mVideoTransmissionThread = null;

				// restart ping
				mPingTimer.start(PING_INTERVAL);
			}

			// reset transmission state
			resetTransmissionState();
		}
		catch (InterruptedException e)
		{
			Log.e("Service", "Video transmission cancelation failed. " + e.getMessage());
		}
	}

	/**
	 * Signals the server that the video rated has been accepted
	 * 
	 * @param videoId
	 * @return
	 */
	public boolean acceptVideoRating(UUID videoId)
	{
		Log.i("Service", "Video rating accepted " + videoId);

		String videoPath = mPreferences.getString(VIDEO_PATH, "");

		deleteVideoFile(new File(videoPath));

		resetTransmissionState();

		return sendMessage(new AcceptRatingMessage(videoId));
	}

	/**
	 * Tries to send the given message. Tries 5 times to reconnect if the
	 * transmission failed. Starts a timer that periodically tries to reconnect
	 * if this method failed to reconnect.
	 * 
	 * @param message
	 * @return true if the message was successfully sent
	 */
	private boolean sendMessage(IMessage message)
	{
		return sendMessage(message, 5);
	}

	/**
	 * Tries to send the given message. Tries to reconnect if the transmission
	 * failed. Starts a timer that periodically tries to reconnect if this
	 * method failed to reconnect.
	 * 
	 * @param message
	 * @param retries
	 *            number of retries before we give up reconnecting, won't stop
	 *            trying to reconnect if retries = 0
	 * @return true if the message was successfully sent
	 */
	private boolean sendMessage(IMessage message, int retries)
	{
		boolean successfulReconnect = false;

		for (int i = 0; i < retries || successfulReconnect || (retries == 0 && !mCancelVideoTransmission); i++)
		{
			try
			{
				mOut.send(message);
				successfulReconnect = true;
				break;
			}
			catch (Exception e)
			{
				Log.e("Service", "Exception " + e.getMessage());

				try
				{
					Thread.sleep(TimeoutTimer.BACKOFF_MIN);
				}
				catch (InterruptedException ie)
				{
					Log.e("Service", "InterruptedException " + ie.getMessage());
				}

				successfulReconnect = reconnect();
			}
		}

		if (!successfulReconnect)
		{
			mTimeoutTimer.startBackOff();
		}

		return successfulReconnect;
	}

	/**
	 * Sets up the connection to the server
	 * 
	 * @throws IOException
	 * @throws SocketTimeoutException
	 */
	private boolean connect() throws IOException, SocketTimeoutException
	{
		try
		{
			mInetAddress = InetAddress.getByName(mPreferences.getString(SERVER_ADDRESS, ""));
		}
		catch (UnknownHostException e)
		{
			Log.e("Service", "UnknownHostException " + e.getMessage());
			return false;
		}

		mPort = mPreferences.getInt(SERVER_PORT, 50001);

		// open tcp connection
		SocketAddress addr = new InetSocketAddress(mInetAddress, mPort);
		mSocket = new Socket();
		mSocket.connect(addr, TIMEOUT_INTERVAL / 2);

		Log.i("Service", "Socket erzeugt " + mSocket);

		// open output and input streams
		mOut = new StrokeOutputWriter(mSocket.getOutputStream());
		mInputListener = new StrokeInputReader(mSocket.getInputStream());

		// register listeners
		mInputListener.setMessageListener(LoginAnswerMessage.class, new LoginAnswerListener());
		mInputListener.setMessageListener(PingMessage.class, new PingListener());
		mInputListener.setMessageListener(AcceptVideoMessage.class, new AcceptVideoListener());
		mInputListener.setMessageListener(VideoStartsMessage.class, new VideoStartsListener());
		mInputListener.setMessageListener(RatingMessage.class, new RatingListener());

		// start input reader
		mInputListener.start(this);

		mIsConnected = true;

		Log.i("Service", "Connected to " + mInetAddress + ":" + mSocket);

		return true;
	}

	/**
	 * Closes the connection to the server.
	 */
	private void disconnect()
	{
		mIsConnected = false;
		mIsLoggedIn = false;

		mPingTimer.cancel();
		mTimeoutTimer.cancel();

		// make sure inputListener is closed before closing outputWriter
		if (mInputListener != null)
		{
			mInputListener.stop();
			mInputListener = null;
		}

		try
		{
			if (mOut != null)
			{
				mOut.close();
				mOut = null;
			}
		}
		catch (Exception e)
		{
			Log.e("Service", "Exception " + e.getMessage());
		}

		try
		{
			if (mSocket != null)
			{
				mSocket.close();
				mSocket = null;
			}
		}
		catch (Exception e)
		{
			Log.e("Service", "Exception " + e.getMessage());
		}

		Log.i("Service", "Disconnected");
	}

	/**
	 * 
	 * @return boolean Indicates that the connection to the server was
	 *         successfully established, NOT a successful reconnect.
	 */
	private boolean reconnect()
	{
		disconnect();
		if (login(mEmail, mPassword))
		{
			mTimeoutTimer.cancel();

			return true;
		}

		return false;
	}

	/**
	 * Reset transmission state in shared preferences.
	 */
	public void resetTransmissionState()
	{
		Editor editor = mPreferences.edit();
		editor.putInt(STATE, TransmissionState.IDLE.ordinal());
		editor.putString(DESCRIPTION, "");
		editor.putString(RATING, "");
		editor.putLong(FILE_SIZE, 0);
		editor.putLong(PROGRESS, 0);
		editor.putString(VIDEO_ID, "");
		editor.commit();
	}

	/**
	 * Is the service connected to the server and logged in?
	 * 
	 * @return
	 */
	public boolean isLoggedIn()
	{
		return mIsLoggedIn;
	}

	/**
	 * Scans for and removes old or broken video files from WhatsStroke's video
	 * folder.
	 */
	private void deleteOldVideos()
	{
		// Check that the SDCard is mounted
		if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
		{
			// Path to the currently used video in the rating process or empty
			String videoPath = mPreferences.getString(VIDEO_PATH, "");

			FileFilter filter = null;

			if (!TextUtils.isEmpty(videoPath))
			{
				final File file = new File(videoPath);

				if (file.exists())
				{
					// Filter out the currently used file
					filter = new FileFilter()
					{
						@Override
						public boolean accept(File pathname)
						{
							return !file.equals(pathname);
						}
					};
				}
			}

			// Path where the videos are saved.
			File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), getText(
					R.string.app_name).toString());

			File[] fileArray = mediaStorageDir.listFiles(filter);

			if (fileArray != null)
			{
				for (File file : fileArray)
				{
					deleteVideoFile(file);
				}
			}
		}
	}

	/**
	 * Deletes the given file if it exists.
	 * 
	 * @param file
	 */
	private void deleteVideoFile(File file)
	{
		if (file != null && file.exists())
		{
			if (!file.delete())
			{
				Log.e("Service", "Could not delete file \"" + file.getAbsolutePath() + "\"");
			}
		}
	}

	/**
	 * Play ringtone and vibrate according to the given pattern.
	 * 
	 * @param pattern
	 */
	private void alert(long[] pattern)
	{
		Uri alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
		if (alert == null)
		{
			// alert is null, using backup
			alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
			if (alert == null)
			{
				// alert is still null, using second backup
				alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
			}
		}

		Ringtone r = RingtoneManager.getRingtone(getApplicationContext(), alert);
		Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);

		r.play();

		if (vibrator != null)
		{
			vibrator.vibrate(pattern, -1);
		}
	}

	/**
	 * Listener for LoginAnswerMessages.<br>
	 * Cancels the timeout timer, then sends a broadcast intent to inform the
	 * activity and starts the ping timer if the login was successful.
	 */
	private class LoginAnswerListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			// cancel timeout timer
			mTimeoutTimer.cancel();

			LoginAnswerMessage answerMessage = (LoginAnswerMessage) message;
			boolean isSuccessful = answerMessage.isSuccessful();

			// start ping timer if login was successful
			if (isSuccessful)
			{
				try
				{
					mOut.send(new PingMessage());
					mIsLoggedIn = true;
				}
				catch (Exception e)
				{
					Log.e("Service", "Exception " + e.getMessage());
				}

				mPingTimer.start(PING_INTERVAL);
			}
			else
			{
				// close reader, writer, socket etc
				logout();
			}

			// inform activity
			Intent intent = new Intent(Broadcast.LOGIN_ANSWER);
			intent.putExtra("de.fhkl.whatsstroke.LoginAnswer", isSuccessful);
			sendBroadcast(intent);

			Log.i("Service", "onLoginAnswer " + isSuccessful);
		}
	}

	/**
	 * Listener for PingMessages.<br>
	 * Cancels the time out timer and updates the status notification.
	 */
	private class PingListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			// cancel timeout
			mTimeoutTimer.cancel();

			if (((PingMessage) message).isReceiverAvailable())
			{
				setNotification(SystemStatus.CONTACT_AVAILABLE);

				Log.i("Service", "Ping Contact available");
			}
			else
			{
				setNotification(SystemStatus.SERVER_AVAILABLE);

				Log.i("Service", "Ping Server available");
			}
		}
	}

	/**
	 * Listener for AcceptVideoMessages.<br>
	 * Saves the time when this message arrived into the shared preferences and
	 * then sends a broadcast intent to inform the activity.
	 */
	private class AcceptVideoListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			String time = DateFormat.getTimeInstance().format(new Date());
			String description = "\n" + time + " " + getText(R.string.video_accepted);

			Editor editor = mPreferences.edit();
			editor.putString(DESCRIPTION, description);
			editor.commit();

			// inform activity
			Intent intent = new Intent(Broadcast.UPDATE_VIEW);
			sendBroadcast(intent);
		}
	}

	/**
	 * Listener for VideoStartsMessages.<br>
	 * Saves the time when this message arrived into the shared preferences and
	 * then sends a broadcast intent to inform the activity.
	 */
	private class VideoStartsListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			String time = DateFormat.getTimeInstance().format(new Date());
			String description = mPreferences.getString(DESCRIPTION, "");
			description += "\n" + time + " " + getText(R.string.video_started);

			Editor editor = mPreferences.edit();
			editor.putString(DESCRIPTION, description);
			editor.commit();

			// inform activity
			Intent intent = new Intent(Broadcast.UPDATE_VIEW);
			sendBroadcast(intent);
		}
	}

	/**
	 * Listener for RatingMessages.<br>
	 * Saves the chosen rating in the shared preferences and then sends a
	 * broadcast intent to inform the activity.
	 */
	private class RatingListener implements IMessageListener
	{
		@Override
		public void onMessage(IMessage message)
		{
			Editor editor = mPreferences.edit();

			if (((RatingMessage) message).getRating())
			{
				editor.putString(RATING, getText(R.string.rating_stroke) + "");
			}
			else
			{
				editor.putString(RATING, getText(R.string.rating_no_stroke) + "");
			}

			editor.commit();

			// play ringtone
			alert(RRRRING);

			// inform activity
			Intent intent = new Intent(Broadcast.UPDATE_VIEW);
			sendBroadcast(intent);
		}
	}

	/**
	 * Ping timer sleeps for a specified time and then sends a ping message.
	 * Repeats until canceled.
	 */
	private class PingTimer extends BroadcastReceiver
	{
		/**
		 * The AlarmManager allows us to schedule an alarm that will wake the
		 * device.
		 */
		private AlarmManager mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

		/**
		 * Indicates that the timer is started. Needed to avoid strange behavior
		 * (occurred only once during testing) on reacting to an already sent
		 * ping during the logout process.
		 */
		private boolean mIsStarted;

		@Override
		public void onReceive(Context context, Intent intent)
		{
			String action = intent.getAction();

			if (action.equals(Broadcast.PING_TIMER) && mIsStarted)
			{
				try
				{
					Log.i("Service", "Ping");

					// send ping message
					mOut.send(new PingMessage());
				}
				catch (Exception e)
				{
					Log.e("Service", "Exception " + e.getMessage());
				}

				// start timeout timer
				mTimeoutTimer.start(TIMEOUT_INTERVAL);
			}
		}

		/**
		 * Starts the ping timer that sleeps for the specified time and then
		 * sends a ping message. Repeats until canceled.
		 * 
		 * @param timeInterval
		 */
		public void start(long timeInterval)
		{
			mIsStarted = true;

			Intent intent = new Intent(Broadcast.PING_TIMER);
			PendingIntent pendingIntent = PendingIntent.getBroadcast(StrokeService.this, 0, intent, 0);

			mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeInterval, timeInterval,
					pendingIntent);
		}

		/**
		 * Stops a previously started ping timer.
		 */
		public void cancel()
		{
			mIsStarted = false;

			Intent intent = new Intent(Broadcast.PING_TIMER);
			PendingIntent pendingIntent = PendingIntent.getBroadcast(StrokeService.this, 0, intent, 0);

			mAlarmManager.cancel(pendingIntent);
		}
	}

	/**
	 * Timeout timer that sleeps for a specified time and then tries to
	 * reconnect. Repeats until reconnect was successful or until canceled.
	 */
	private class TimeoutTimer extends BroadcastReceiver
	{
		/** Start back off strategy with a sleep time of 2 seconds */
		public static final int BACKOFF_MIN = 2000;

		/** Stop increasing sleep time at 30 seconds */
		public static final int BACKOFF_MAX = 30000;

		/**
		 * The AlarmManager allows us to schedule an alarm that will wake the
		 * device.
		 */
		private AlarmManager mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

		/** Current sleep time */
		private int mBackoffTimeout = BACKOFF_MIN;

		/**
		 * Indicates that the timer is started. Needed to avoid strange behavior
		 * (occurred only once during testing) on reacting to an already sent
		 * ping during the logout process.
		 */
		private boolean mIsStarted;

		@Override
		public void onReceive(Context context, Intent intent)
		{
			String action = intent.getAction();

			if (action.equals(Broadcast.TIMEOUT_TIMER) && mIsStarted)
			{
				setNotification(SystemStatus.OFFLINE);

				Log.w("Service", "Ping timed out, reconnecting");

				if (!reconnect())
				{
					mTimeoutTimer.start(mBackoffTimeout);

					mBackoffTimeout = Math.min(mBackoffTimeout * 2, BACKOFF_MAX);
				}
			}
		}

		/**
		 * Starts the timeout timer that sleeps for a specified time and then
		 * tries to reconnect. Repeats until reconnect was successful or until
		 * canceled.<br>
		 * Doubles sleep time after every failed reconnect.
		 */
		public void startBackOff()
		{
			start(mBackoffTimeout);
			mBackoffTimeout = Math.min(mBackoffTimeout * 2, BACKOFF_MAX);
		}

		/**
		 * Starts the timeout timer that sleeps for the specified time and then
		 * tries to reconnect. Repeats until reconnect was successful or until
		 * canceled.
		 * 
		 * @param timeInterval
		 */
		public void start(long timeInterval)
		{
			mIsStarted = true;

			Intent intent = new Intent(Broadcast.TIMEOUT_TIMER);
			PendingIntent pendingIntent = PendingIntent.getBroadcast(StrokeService.this, 0, intent, 0);

			mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeInterval, pendingIntent);
		}

		/**
		 * Stops a previously started timeout timer.
		 */
		public void cancel()
		{
			mIsStarted = false;

			Intent intent = new Intent(Broadcast.TIMEOUT_TIMER);
			PendingIntent pendingIntent = PendingIntent.getBroadcast(StrokeService.this, 0, intent, 0);

			mAlarmManager.cancel(pendingIntent);

			mBackoffTimeout = BACKOFF_MIN;
		}
	}

	/**
	 * Allows activities to get a reference of the service.
	 */
	public class StrokeBinder extends Binder
	{
		private WeakReference<StrokeService> mService;

		public StrokeBinder(StrokeService service)
		{
			mService = new WeakReference<StrokeService>(service);
		}

		public StrokeService getService()
		{
			return mService.get();
		}
	}
}